/*
 * Project: a2
 * Copyright (C) 2008 ralfoide gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.alfray.a2.gameplay;

import java.util.concurrent.atomic.AtomicInteger;

import com.alfray.a2.engine.BaseThread;



class AnimThread extends BaseThread {

    public static final int FPS = 30;
    public static final int FPS_MS = 1000/FPS;
    @SuppressWarnings("unused") private static final String TAG = "ActionThread";

    private long mCounter = 0;
    private long mLastDeltaTimeMs;
    private AtomicInteger mAnimPending;
    private GameState mGameState;

    public AnimThread(GameState gameState) {
        super("AnimThread");
        mGameState = gameState;
        mAnimPending = new AtomicInteger(0);

        this.setPriority(Thread.currentThread().getPriority() + 1);
    }

    public float getEstimatedFps() {
        if (mLastDeltaTimeMs > 0) {
            return 1000.0f / mLastDeltaTimeMs;
        }
        return 0;
    }

    // -----------------



    @Override
    public void run() {

        while (mContinue) {

            if (pauseWait() && !mContinue) break;

            long startMs = System.currentTimeMillis();

            processAnims();

            long waitMs = FPS_MS - (System.currentTimeMillis() - startMs);
            if (waitMs > 0) {
                try {
                    sleep(waitMs);
                } catch (InterruptedException e) {
                    // pass
                }
            }

            mLastDeltaTimeMs = System.currentTimeMillis() - startMs;
        }
    }

    private void processAnims() {
        long counter = ++mCounter;

        Cell[] cells = mGameState.mBoard;

        SpriteState sprite = null;
        AnimType type = null;
        AnimData data = null;

        for (int index = cells.length - 1;
                index >= 0 && mAnimPending.get() > 0 && mContinue;
                index--) {
            synchronized(this) {
                sprite = cells[index].mSprite;
            }

            synchronized(sprite) {
                type = sprite.mAnimType;
                if (type == null) continue;
                data = (AnimData) sprite.mAnimData;
                if (data == null) continue;

                type.apply(this, sprite, counter);
            }
        }
    }

    //---------------

    /** The type of animation associated with a cell */
    public static enum AnimType {
        /** Blinks the sprite on and off at the repeat interval */
        BLINKING {
            @Override
            public void apply(AnimThread anim, SpriteState sprite, long counter) {
                AnimData data = (AnimData) sprite.mAnimData;
                long next = data.mNext;
                if (counter >= next) {
                    data.mNext = next + data.xDeltaX;
                    sprite.setVisible(!sprite.mIsVisible);
                }
            }

            @Override
            public void onStop(AnimThread anim, SpriteState sprite) {
                if (!sprite.mIsVisible) {
                    sprite.setVisible(true);
                    sprite.setAnimType(null);
                }
                super.onStop(anim, sprite);
            }
        },

        /** Shrink animation based on inset of the containing sprite rectangle.
         *  Once shrank completed, the sprite is made invisible. */
        SHRINK {
            @Override
            public void apply(AnimThread anim, SpriteState sprite, long counter) {
                if (!sprite.mIsVisible) {
                    return;
                }

                AnimData data = (AnimData) sprite.mAnimData;
                long next = data.mNext;
                long n = counter - next;
                if (n > 0) {
                    int i = data.mNumFrames - (int)n;
                    if (i < 0) {
                        onStop(anim, sprite);
                        return;
                    }

                    data.mNumFrames = i;
                    data.mNext = counter;

                    int d = data.xDeltaX * (int)n;
                    i = sprite.xZoomXY;
                    sprite.setZoomXY(i - d);
                }
            }

            @Override
            public void onStop(AnimThread anim, SpriteState sprite) {
                AnimData data = (AnimData) sprite.mAnimData;
                // xDeltaX is positive for a shrink: not visible at end
                // xDeltaX is negative for a appear: visible at end
                sprite.setVisible(data.xDeltaX < 0);
                sprite.setZoomXY(1<<16);

                super.onStop(anim, sprite);
            }
        },

        /** Fly-over animation based on modifying the offset of the sprite at
         * each frame. Ignore invisible cells. */
        FLYOVER {
            @Override
            public void apply(AnimThread anim, SpriteState sprite, long counter) {
                if (!sprite.mIsVisible) {
                    return;
                }

                AnimData data = (AnimData) sprite.mAnimData;
                long next = data.mNext;
                long n = counter - next;
                if (n > 0) {
                    int i = data.mNumFrames - (int)n;
                    if (i < 0) {
                        onStop(anim, sprite);
                        return;
                    }

                    data.mNumFrames = i;
                    data.mNext = counter;
                    i = (int)n;

                    int dx = data.xDeltaX;
                    int dy = data.xDeltaY;

                    int sx = data.mCurrentX;
                    int sy = data.mCurrentY;

                    sx += dx * i;
                    sy += dy * i;
                    data.mCurrentX = sx;
                    data.mCurrentY = sy;

                    sprite.setOffset(sx, sy);
                }
            }

            @Override
            public void onStop(AnimThread anim, SpriteState sprite) {
                sprite.setOffset(0, 0);
                super.onStop(anim, sprite);
            }
        };

        /**
         * Applies the animation. This is not synchronized.
         * @return The next time_ms to execute or 0 if the anim is completed.
         */
        public abstract void apply(AnimThread anim, SpriteState sprite, long counter);

        /**
         * Called when the animation is going to be stopped. The anim type
         * should put the sprite in the desired final state.
         */
        public void onStop(AnimThread anim, SpriteState sprite) {
            sprite.setAnimType(null);
            AnimData data = (AnimData) sprite.mAnimData;
            data.clear();
            anim.mAnimPending.decrementAndGet();
        }
    }

    private static class AnimData {
        public long mLong;
        public long mNext;
        public int xDeltaX;
        public int xDeltaY;
        public int mCurrentX;
        public int mCurrentY;
        public int mNumFrames;

        /** Removes object references to prevent GC leaks */
        public void clear() {
            xDeltaX = xDeltaY = mCurrentX = mCurrentY = mNumFrames = 0;
            mLong = mNext = 0;
        }
    }

    /**
     * Stops the animation associated with a given sprite.
     *
     * @param sprite The sprite to stop animate
     */
    public void stopAnim(SpriteState sprite) {
        if (sprite == null) return;

        synchronized(sprite) {
            AnimType type = sprite.mAnimType;
            if (type != null) {
                AnimData data = (AnimData) sprite.mAnimData;
                type.onStop(this, sprite);
                sprite.setAnimType(null);
                if (data != null) data.clear();
            }
        }
    }

    /** Prepare the sprite for a new animation. Stop any current animation,
     *  setup the data if it doesn't exists or clear the existing one. */
    private AnimData beginPrepare(SpriteState sprite) {
        stopAnim(sprite);

        AnimData data = null;
        boolean need_clear = false;
        synchronized(sprite) {
            if (sprite.mAnimType != null) {
                sprite.setAnimType(null);
                need_clear = true;
            }
            data = (AnimData) sprite.mAnimData;
        }

        if (data == null) {
            data = new AnimData();
        } else if (need_clear) {
            data.clear();
        }
        return data;
    }

    /** Caller MUST be synchronized on sprite */
    private void endPrepare(SpriteState sprite, AnimType type, AnimData data) {
        sprite.setAnimType(type);
        sprite.setAnimData(data);
        mAnimPending.incrementAndGet();
    }

    /**
     * Starts a blinking animation with the corresponding repeat interval.
     * When the animation completes, the sprite is visible.
     *
     * @param sprite The sprite to animate.
     * @param numFps Blinking interval (1 on or 1 off) in frames, based on AnimThread.FPS (~10 fps).
     *
     * @deprecated
     */
    public void startBlinking(SpriteState sprite, int numFrames) {
        if (sprite == null || numFrames < 0) return;

        AnimData data = beginPrepare(sprite);

        data.mNext = mCounter + numFrames;
        data.xDeltaX = numFrames;

        synchronized(sprite) {
            sprite.setVisible(false);
            endPrepare(sprite, AnimType.BLINKING, data);
        }
    }

    /**
     * Calls startBlinking on the first count sprites of the given array.
     * The whole thing is synchronized.
     *
     * @deprecated
     */
    public void startBlinking(Cell[] cells, int count, int numFrames) {
        synchronized(this) {
            for (int i = 0; i < count; i++) {
                startBlinking(cells[i].mSprite, numFrames);
            }
        }
    }

    /**
     * Starts a shrinking zoom animation.
     * When the animation completes, the sprite is invisible.
     *
     * If numFrames are zero, no animation happens.
     *
     * @param sprite The sprite to animate.
     * @param numFrames The number of frames, based on AnimThread.FPS (~10 fps)
     */
    public void startShrink(SpriteState sprite, int numFrames) {
        if (sprite == null || numFrames <= 0) return;

        AnimData data = beginPrepare(sprite);

        data.mNext = mCounter;
        data.xDeltaX = (1<<16) / numFrames;
        data.mNumFrames = numFrames;

        synchronized(sprite) {
            sprite.setZoomXY(1<<16);
            endPrepare(sprite, AnimType.SHRINK, data);
        }
    }

    /**
     * Calls startShrink on the first count sprites of the given array.
     * The whole thing is synchronized.
     */
    public void startShrink(Iterable<Cell> cells, int numFrames) {
        synchronized(this) {
            for (Cell cell : cells) {
                startShrink(cell.mSprite, numFrames);
            }
        }
    }

    /**
     * Starts an "appears" zoom animation (reverse of shrink)
     * When the animation completes, the sprite is visible.
     *
     * If numFrames are zero, no animation happens.
     *
     * @param sprite The sprite to animate.
     * @param numFrames The number of frames, based on AnimThread.FPS (~10 fps)
     */
    public void startAppears(SpriteState sprite, int numFrames) {
        if (sprite == null || numFrames <= 0) return;

        AnimData data = beginPrepare(sprite);

        data.mNext = mCounter;
        data.xDeltaX = -(1<<16) / numFrames;
        data.mNumFrames = numFrames;

        synchronized(sprite) {
            sprite.setZoomXY(0);
            endPrepare(sprite, AnimType.SHRINK, data);
        }
    }

    /**
     * Starts a fly-over animation, based on changing the position of the sprite
     * by x/y pixels at each frame. The initial modified position is computed so
     * that at the end of numFrames, the sprite achieves its real position, so
     * it always flies from "away" to "home". When the animation completes the
     * sprite is visible in its real position.
     *
     * If both xPxOffset and yPxOffset or numFrames are zero, no animation happens.
     *
     * @param sprite The sprite to animate.
     * @param xOffsetX The number of X pixels to move at each frame.
     * @param xOffsetY The number of Y pixels to move at each frame.
     * @param numFrames The number of frames, based on AnimThread.FPS (~30 fps)
     */
    public void startFlyOver(SpriteState sprite, int xOffsetX, int xOffsetY, int numFrames) {
        if (sprite == null || numFrames <= 0) return;
        if (xOffsetX == 0 && xOffsetY == 0) return;

        AnimData data = beginPrepare(sprite);

        int sx = 0 - xOffsetX * numFrames;
        int sy = 0 - xOffsetY * numFrames;

        data.mNext = mCounter;
        data.xDeltaX = xOffsetX;
        data.xDeltaY = xOffsetY;
        data.mCurrentX = sx;
        data.mCurrentY = sy;
        data.mNumFrames = numFrames;

        synchronized(sprite) {
            sprite.setOffset(sx, sy);
            endPrepare(sprite, AnimType.FLYOVER, data);
        }
    }

}


